package cadtoolbox.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Stroke;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.awt.Paint;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.ProgressMonitor;
import javax.swing.SwingWorker;

import cadtoolbox.utils.MyEventHandler;
import cadtoolbox.utils.MyStepHandler;
import cadtoolbox.utils.MyWorker;
import cadtoolbox.utils.PluggableWorker;

import org.apache.commons.collections15.Transformer;

import org.apache.commons.math3.ode.ContinuousOutputModel;
import org.apache.commons.math3.ode.FirstOrderDifferentialEquations;
import org.apache.commons.math3.ode.events.EventHandler;
import org.apache.commons.math3.ode.nonstiff.DormandPrince853Integrator;
import org.apache.commons.math3.ode.nonstiff.GraggBulirschStoerIntegrator;
import org.jfree.ui.RefineryUtilities;

import edu.uci.ics.jung.algorithms.layout.CircleLayout;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.util.EdgeType;
import edu.uci.ics.jung.visualization.BasicVisualizationServer;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse;
import edu.uci.ics.jung.visualization.control.ModalGraphMouse;
import edu.uci.ics.jung.visualization.renderers.Renderer.VertexLabel.Position;


public class OligoSystem<E> implements Serializable,
		Cloneable, FirstOrderDifferentialEquations {

	/**
	 * 
	 */
	private static final long serialVersionUID = -3626332588448633729L;

	private double observedExoKm = 0;

	public int total = 0;
	public int inhTotal = 0;
	
	public Map<E,Template<E>> templates;
	
	protected OligoGraph<SequenceVertex,E> graph;

	protected LinkedHashSet<SequenceVertex> sequences;
	
	private int profiling = 0;
	private double[][] savedActivity = new double[3][Constants.numberOfPoints+1];
	private int time = 0;

	/**
	 * Created everytime we try to integrate the system over time
	 * @param graph
	 */
	public OligoSystem(OligoGraph<SequenceVertex,E> graph) {
		this.graph = graph;
		this.inhTotal = graph.inhibitors.size();
		this.total = graph.getVertexCount() - this.inhTotal;

		this.templates = new LinkedHashMap<E,Template<E>>();
		for (E e :graph.getEdges()){
			this.templates.put(e, new Template<E>(graph,graph.templateConcentrations.get(e),graph.getSource(e),graph.getDest(e),(graph.getInhibition(e)==null?null:graph.getInhibition(e).getLeft())));
		}
		this.sequences = new LinkedHashSet<SequenceVertex>(graph.getVertices());
	}

	public void setTotals(int total, int inhTotal) {
		this.total = total;
		this.inhTotal = inhTotal;
	}
	
	public double getTotalCurrentFlux(SequenceVertex s) {
		// System.out.println("Current concentretation for "+s+": "+s.getConcentration());
		if (!graph.isInhibitor(s)) {
			return getTotalCurrentFluxSimple(s);
		} else {
			return getTotalCurrentFluxInhibitor(s);
		}
	}

	private double getTotalCurrentFluxSimple(SequenceVertex s) {
		// As Input
		double flux = 0;
		Template<E> temp;

		for(E e: graph.getOutEdges(s)){
			temp = this.templates.get(e);
			flux += temp.inputSequenceFlux();
			}
		// As Output
		for (E e: graph.getInEdges(s)) {
			temp = this.templates.get(e);
			flux += temp.outputSequenceFlux();
		}
		if (!graph.saturableExo) {

			flux -= s.getConcentration() * (Constants.exoConc*Constants.exoVm)/Constants.exoKmSimple;
		} else {
			//flux -= s.getConcentration() * Constants.exoConc*Constants.exoVm
			//		/ (Constants.exoKmSimple * this.observedExoKm);
			if(graph.coupling){
			flux -= s.getConcentration() * Constants.exoConc*Constants.exoVm/ this.computeExoKm(Constants.exoKmSimple);
			} else {
			flux -= s.getConcentration() * Constants.exoConc*Constants.exoVm/(Constants.exoKmSimple + s.getConcentration());
		}
			
//			double obsExoKm, value = 0;
//			for (SequenceVertex su: graph.getVertices()) {
//				value += su.getConcentration()
//						/ (graph.inhibitors.containsKey(su)?Constants.exoKmInhib:Constants.exoKmSimple);
//			}
//			obsExoKm = 1 + value;
//			flux -= s.getConcentration() * Constants.exoConc*Constants.exoVm
//					/ (Constants.exoKmSimple * obsExoKm);
		}
		return flux;
	}

	private double getTotalCurrentFluxInhibitor(SequenceVertex s) {
		double flux = 0;
		Template<E> temp;
		// As Inhib
		temp = this.templates.get(graph.inhibitors.get(s));
		flux += temp.inhibSequenceFlux();
		
		// As Output
		for (E e: graph.getInEdges(s)) {
			temp = this.templates.get(e);
			flux += temp.outputSequenceFlux();
		}
		if (!graph.saturableExo) {

			flux -= s.getConcentration() * Constants.exoConc*Constants.exoVm/Constants.exoKmInhib;

		} else {
			//flux -= s.getConcentration() * Constants.exoConc*Constants.exoVm
			/// (Constants.exoKmInhib * this.observedExoKm);
			if(graph.coupling){
			flux -= s.getConcentration() * Constants.exoConc*Constants.exoVm/ this.computeExoKm(Constants.exoKmInhib);
			} else {
				flux -= s.getConcentration() * Constants.exoConc*Constants.exoVm/(Constants.exoKmInhib + s.getConcentration());
			}
//			double obsExoKm, value = 0;
//			for (SequenceVertex su: graph.getVertices()) {
//				value += su.getConcentration()
//						/ (graph.inhibitors.containsKey(su)?Constants.exoKmInhib:Constants.exoKmSimple);
//			}
//			obsExoKm = 1 + value;
//			flux -= s.getConcentration() * Constants.exoConc*Constants.exoVm
//					/ (Constants.exoKmInhib * obsExoKm);
		}

		return flux;
	}

	private double computeExoKm(double myKm){
		double value = myKm;
		for (SequenceVertex s: graph.getVertices()) {
			double tempKm = (graph.inhibitors.containsKey(s)?Constants.exoKmInhib:Constants.exoKmSimple);
			value += s.getConcentration()*(tempKm==myKm?1.0:myKm/tempKm);
		}
		if(graph.exoSaturationByFreeTemplates){
			for(Template<E> t: this.templates.values()){
				value += (myKm*t.concentrationAlone)/Constants.exoKmTemplate;
			}
		}
		return value;
	}
	
	private void setObservedExoKm() {
		double value = 0;
		for (SequenceVertex s: graph.getVertices()) {
			value += s.getConcentration()
					/ (graph.inhibitors.containsKey(s)?Constants.exoKmInhib:Constants.exoKmSimple);
		}
		if(graph.exoSaturationByFreeTemplates){
			for(Template<E> t: this.templates.values()){
				value += t.concentrationAlone/Constants.exoKmTemplate;
			}
		}
		this.observedExoKm = 1 + value;
	}
	
	private void setObservedPolyKm() {
		double value = 1;
		if(graph.coupling){
		for (Template<E> temp: this.templates.values()){
			value += temp.getPolyUsage();
		}
		for (Template<E> temp: this.templates.values()){
			//System.out.println("New pol activity: "+Constants.polVm/(Constants.polKm*value));
			temp.setAllocatedPoly(value);
		}
		} else {
			for (Template<E> temp: this.templates.values()) {
				value =1+ temp.getPolyUsage();
				//perceivedPoly = new Enzyme("poly", this.poly.activity/value, this.poly.basicKm, this.poly.optionalValue);
				temp.setAllocatedPoly(value);
		}
	}
	}
	
	private void setObservedNickKm() {
		double value = 1;
		if(graph.coupling){
		for (Template<E> temp: this.templates.values()){
			value += temp.getNickUsage();
		}
		for (Template<E> temp: this.templates.values()){
			temp.setAllocatedNick(value);
		}
		} else {
			for (Template<E> temp: this.templates.values()) {
				value =1+ temp.getNickUsage();
				//perceivedPoly = new Enzyme("poly", this.poly.activity/value, this.poly.basicKm, this.poly.optionalValue);
				temp.setAllocatedNick(value);
		}
	}
	}

	private double[] getCurrentConcentration() {
		double concentration[] = new double[total + inhTotal];
		Iterator<SequenceVertex> it = this.sequences.iterator();
		int i=0;
		while (it.hasNext()) {
					concentration[i] = it.next().getConcentration();
					i++;
				}
		return (concentration);
	}

	private void setCurrentConcentration(double concentration[]) {
		try {
			Iterator<SequenceVertex> it = this.sequences.iterator();
			int i=0;
			while (it.hasNext()) {
						it.next().setConcentration(concentration[i]);
						i++;
			}
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(1);
		}
	}

	private double[] getCurrentFlux() {
		double flux[] = new double[total + inhTotal];
		Iterator<SequenceVertex> it = this.sequences.iterator();
		int i=0;
		while (it.hasNext()) {
					flux[i] = this.getTotalCurrentFlux(it.next());
					i++;
				}
		return flux;
	}

	private ArrayList<double[]> getCurrentInternalStates() {
		ArrayList<double[]> internals = new ArrayList<double[]>();
		Iterator<Template<E>> it = this.templates.values().iterator();
		while (it.hasNext()) {
			internals.add(it.next().getStates().clone());
				}
		return internals;
	}

	private ArrayList<double[]> getCurrentInternalFlux() {
		ArrayList<double[]> internals = new ArrayList<double[]>();
		Iterator<Template<E>> it = this.templates.values().iterator();
		while (it.hasNext()) {
			internals.add(it.next().flux().clone());
				}
		return internals;
	}

	private void setInternalStates(ArrayList<double[]> internals)
			throws InvalidConcentrationException {
		int i=0;
		Iterator<Template<E>> it = this.templates.values().iterator();
		while (it.hasNext()) {
			it.next().setStates(internals.get(i));
			i++;
		}
	}

	public void reinitializeOiligoSystem() {
		for(SequenceVertex v: graph.getVertices()){
			v.reset();
		}
		for(Template<E> temp: templates.values()){
			temp.reset();
		}
		this.savedActivity = new double[3][Constants.numberOfPoints+1];
	}

	@Override
	public void computeDerivatives(double t, double[] y, double[] ydot) {
		// First all the activation sequences, then inibiting, then templates.
		// ydot is a placeholder, should be updated with the derivative of y at
		// time t.
		
		profiling++;
		
		int where = 0;
		boolean saveActivity = (t >= this.time +1);
		Iterator<SequenceVertex> it = this.sequences.iterator();
		while(it.hasNext()){
			
						it.next().setConcentration(y[where]);
			
						where++;
			}

		double[] internal = new double[6];
		Iterator<Template<E>> it2 = this.templates.values().iterator();
		while(it2.hasNext()) {
			internal[0] = y[where+0]; // there must be a better way. There is with arraycopy...
			internal[1] = y[where+1];
			internal[2] = y[where+2];
			internal[3] = y[where+3];
			internal[4] = y[where+4];
			internal[5] = y[where+5];
			where = where + 6;
			try {
				it2.next().setStates(internal);
			} catch (InvalidConcentrationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				System.exit(-1);
			}
		}
		
//		if (graph.saturableExo){
//			this.setObservedExoKm();
//		}
		if (graph.saturablePoly){
			this.setObservedPolyKm();
		}
		if (graph.saturableNick){
			this.setObservedNickKm();
		}
		
		if (saveActivity){
			//System.out.println("Saving stuff "+this.time);
			this.time++;
			this.savedActivity[0][time] = graph.saturableExo?Constants.exoKmSimple/ this.computeExoKm(Constants.exoKmSimple):1;
			//ret[where] = graph.saturablePoly?this.templates.values().iterator().next().poly:1;
			this.savedActivity[1][time] = graph.saturablePoly?this.templates.values().iterator().next().poly/(Constants.polVm/Constants.polKm):1;
			//ret[where] = graph.saturablePoly?this.templates.values().iterator().next().nick:1;
			this.savedActivity[2][time] = graph.saturablePoly?this.templates.values().iterator().next().nick/(Constants.nickVm/Constants.nickKm):1;
		}
		
		where = 0;
		it = this.sequences.iterator();
		SequenceVertex seq;
		while(it.hasNext()){
					seq = it.next();
					ydot[where] = this.getTotalCurrentFlux(seq);
					for(AbstractInput inp : seq.inputs){
						ydot[where]+=inp.f(t);
					}
					where++;
				}

//		if (this.inputs != null) {
//			for (int i = 0; i < this.inputs.length; i++) {
//				ydot[inputs[i].receivingSeq] = ydot[inputs[i].receivingSeq]
//						+ inputs[i].ammount
//						* Math.exp(-(t - inputs[i].timeStep)
//								* (t - inputs[i].timeStep) / 10)
//						/ Math.sqrt(10) / Math.sqrt(Math.PI);
//				//System.out.println("ydot of input "+i+" :"+ ydot[inputs[i].receivingSeq]);
//			}
//		}

		it2 = this.templates.values().iterator();
		while(it2.hasNext()) {
			internal = it2.next().flux();
			ydot[where + 0] = internal[0];
			ydot[where + 1] = internal[1];
			ydot[where + 2] = internal[2];
			ydot[where + 3] = internal[3];
			ydot[where + 4] = internal[4];
			ydot[where + 5] = internal[5];
			if(internal[0]+internal[1]+internal[2]+internal[3]+internal[4]+internal[5]>1e-20){
				//System.out.println("Nooooooooo time: "+t+" "+internal[0]+" "+internal[1]+" "+internal[2]+" "+internal[3]+" "+internal[4]+" "+internal[5]+" "+ydot[0]);
			} else {
				//System.out.println("Ok time: "+t+" "+internal[0]+" "+internal[1]+" "+internal[2]+" "+internal[3]+" "+internal[4]+" "+internal[5]+" "+ydot[0]);
			}
			where = where + 6;
		}
		//if(t>=10)
		//System.out.println("Alternative test: time "+t+" [y] "+y[0]+" d "+ydot[0]);
		where++;
	}

	/**
	 * dimension: signal sequences + inhibiting sequences + 6 states per template + 3 enzymes
	 * enzymes are not calculated by the integrator: always get a derivative of 0.
	 */
	@Override
	public int getDimension() {
		
		return this.total + this.inhTotal + this.templates.size() * 6;
	}

	public double[] initialConditions() {
//		if (graph.saturableExo){
//			this.setObservedExoKm();
//		}
		if (graph.saturablePoly){
			this.setObservedPolyKm();
		}
		if (graph.saturableNick){
			this.setObservedNickKm();
		}
		double[] ret = new double[this.getDimension()];
		int where = 0;
		for (int i = 0; i < this.total + this.inhTotal; i++) {
					ret[where] = this.getCurrentConcentration()[i];
					
					where++;
				}
		
		double[] internal = new double[6];
		Iterator<Template<E>> it2 = this.templates.values().iterator();
		while(it2.hasNext()){
			internal = it2.next().getStates();
			ret[where + 0] = internal[0];
			ret[where + 1] = internal[1];
			ret[where + 2] = internal[2];
			ret[where + 3] = internal[3];
			ret[where + 4] = internal[4];
			ret[where + 5] = internal[5];
			where = where + 6;
		}
		//System.out.println("Oui?");
		//ret[where] = graph.saturableExo?Constants.exoVm/ this.computeExoKm(Constants.exoKmSimple):1;
		this.savedActivity[0][0] = graph.saturableExo?Constants.exoKmSimple/ this.computeExoKm(Constants.exoKmSimple):1;
		//ret[where] = graph.saturablePoly?this.templates.values().iterator().next().poly:1;
		this.savedActivity[1][0] = graph.saturablePoly?this.templates.values().iterator().next().poly/(Constants.polVm/Constants.polKm):1;
		//ret[where] = graph.saturablePoly?this.templates.values().iterator().next().nick:1;
		this.savedActivity[2][0] = graph.saturablePoly?this.templates.values().iterator().next().nick/(Constants.nickVm/Constants.nickKm):1;
		return ret;
	}

	public double[][] calculateTimeSeries(PluggableWorker myWorker) {
		final GraggBulirschStoerIntegrator myIntegrator = new GraggBulirschStoerIntegrator(
				1e-10, 1, Constants.absprec, Constants.relprec);
		// DormandPrince853Integrator myIntegrator = new
		// DormandPrince853Integrator(1e-18, 10000, Constants.absprec, Constants.relprec) ;
		
		//int index = 0;
		//ArrayList<SequenceVertex> allseqs = new ArrayList<SequenceVertex>(sequences);
		//allseqs.get(0).inputs.add(new RampeInput());
		
		this.reinitializeOiligoSystem();
		//System.out.println("reinitialized");
		final double[] placeholder = this.initialConditions();
		final OligoSystem syst = this;
		final MyStepHandler handler = new MyStepHandler();
		
		handler.registerWorker(myWorker);
		
		
		
//		HashMap<SequenceVertex,Integer> inp = new HashMap<SequenceVertex,Integer>();
//		for (SequenceVertex seq: allseqs){
//			if(!seq.inputs.isEmpty()){
//				inp.put(seq, allseqs.indexOf(seq));
//			}
//		}
//		MyEventHandler ehandler = new MyEventHandler(inp);
		myIntegrator.addStepHandler(handler);
		//myIntegrator.addEventHandler(ehandler, 100, 1e-10, 100);
		//System.out.println("Ready");
		myIntegrator.integrate(syst, 0, placeholder, Constants.numberOfPoints,
				placeholder);
		syst.displayProfiling();
		
		//progressMonitor.close();
		return handler.getTimeSerie();
		
	}
	
	public double[][] getActivity(){
		return this.savedActivity;
	}
	
	public void displayProfiling(){
		System.out.println("profiling: "+profiling+" steps");
	}
	
	public String[] giveNames() {
		String[] names = new String[graph.getPlottedSeqs().size()];
		int index = 0;
		Iterator<SequenceVertex> it = graph.getPlottedSeqs().iterator();
		
		while(it.hasNext()){
			
			names[index] = it.next().toString();
			index++;
			
		}
		return names;
	}
	
	public Set<SequenceVertex> getSequences(){
		return this.sequences;
	}
	
	public Collection<Template<E>> getTemplates(){
		return this.templates.values();
	}
	
	public OligoGraph<SequenceVertex,E> getGraph(){
		return this.graph;
	}
}
